package ch.bfh.abcm.rems.sensors.tinkerforge;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;

import org.apache.log4j.Logger;

import ch.bfh.abcm.rems.exceptions.FountNotAllSensorsException;
import ch.bfh.abcm.rems.interfaces.SensorController;
import ch.bfh.abcm.rems.interfaces.tinkerforge.TFSensor;
import ch.bfh.abcm.rems.interfaces.tinkerforge.TFSensorDataPersister;
import ch.bfh.abcm.rems.sensors.tinkerforge.tfsensors.REMSAmbientLightSensor;
import ch.bfh.abcm.rems.sensors.tinkerforge.tfsensors.REMSDistanceIRSensor;
import ch.bfh.abcm.rems.sensors.tinkerforge.tfsensors.REMSHumiditySensor;
import ch.bfh.abcm.rems.sensors.tinkerforge.tfsensors.REMSMoistureSensor;
import ch.bfh.abcm.rems.sensors.tinkerforge.tfsensors.REMSMotionSensor;
import ch.bfh.abcm.rems.sensors.tinkerforge.tfsensors.REMSTempSensor;
import ch.bfh.abcm.rems.sensors.tinkerforge.tfsensors.REMSTemperatureIRSensor;

import com.tinkerforge.AlreadyConnectedException;
import com.tinkerforge.IPConnection;
import com.tinkerforge.IPConnection.EnumerateListener;
import com.tinkerforge.NotConnectedException;

public class TFSensorController implements SensorController {

    public static final long CALL_BACK_PERIOD = 1000;
    public static final int ENUMMERATION_TIMEOUT = 500;
    public static final int EXPECTED_NUMBER_OF_SENSORS_FOR_ONE_MASTEBRICK = 3;

    public static final int DEFAULT_PORT = 4223;

    private static Logger logger = Logger.getLogger("play");

    private ArrayList<TFSensor> sensors;
    private TFSensorDataPersister persister;
    private ArrayList<TFSensorConfigDefinition> sensorDefinitions;
    private ArrayList<String> masterBrickIPs;

    public TFSensorController(TFConfig config) {
        this.persister = config.getDataPersister();
        this.masterBrickIPs = config.getMasterbrickIPs();
        this.sensors = new ArrayList<TFSensor>();
        this.sensorDefinitions = new ArrayList<TFSensorConfigDefinition>();
    }

    public boolean isSensorConnected(String uid){
        for (TFSensor sensor : this.sensors) {
            logger.debug("Compare uid{"+uid+"} with sensor.getUID(){"+sensor.getUID()+"}");
            if (sensor.getUID().equalsIgnoreCase(uid)) {
                logger.debug("Sensor is Connected: "+sensor.isConnected());
                return sensor.isConnected();
            }
        }
        logger.warn("found no sensor, returnig false");
        return false;
    }
    
    @Override
    public void connectToAvailableSensors() {
        logger.debug("Amountof Sensors in List: " + sensors.size());
        for (TFSensor tfSensor : sensors) {
            tfSensor.connect();
            tfSensor.setCallBackPeriod(tfSensor.getCallBackPeriod());
        }
    }

    @Override
    public void disconnectSensors() {
        for (TFSensor tfSensor : sensors) {
            tfSensor.disconnect();
        }
    }

    @Override
    public synchronized void updateSensorList() {
        logger.info("Start updateSensorList...");
        logger.debug("Disconnect All Sensors, (ignore errors");
        disconnectSensors();
        logger.info("empty SensorDefinition List");
        this.sensorDefinitions.clear();
        logger.info("Search new TFSensors and add to List");
        ArrayList<IPConnection> searchConnectiionsArrayList = new ArrayList<IPConnection>();
        for (String masterBrickIP : masterBrickIPs) {
            searchConnectiionsArrayList.add(createSearchIPConnectionWithEnnumerationListener(masterBrickIP));
        }
        int amountOfExpectedSensors = (masterBrickIPs.size() * EXPECTED_NUMBER_OF_SENSORS_FOR_ONE_MASTEBRICK);
        foundAllExpectedSensors(amountOfExpectedSensors);
        logger.info("SensorList is updated, creating the Sensorlist");
        sensors = createTFSensors();
        for (IPConnection ipConnection : searchConnectiionsArrayList) {
            try {
                ipConnection.disconnect();
            } catch (NotConnectedException e) {

            }
        }
        logger.info("Update the SensorConfig in the DB");
        persister.sensorConfigurationHasChanged(sensorDefinitions);

    }

    private boolean foundAllExpectedSensors(int amountOfExpectedSensors) {
        logger.debug("AmountOfExpectedSensors: " + amountOfExpectedSensors);
        waitForTheSensorsToConnect(amountOfExpectedSensors);
        boolean foundAll = (amountOfExpectedSensors == sensorDefinitions.size());
        logger.debug("Found ALL: " + foundAll);
        if (!foundAll) {
            String message = "Amount Expected Sensors: " + amountOfExpectedSensors + " found: "
                    + sensorDefinitions.size();
            logger.error(message);
            throw new FountNotAllSensorsException(message);
        }
        return foundAll;
    }

    private IPConnection createSearchIPConnectionWithEnnumerationListener(String masterBrickIP) {
        IPConnection searchConnection = new IPConnection();
        try {
            searchConnection.setTimeout(TFSensorController.ENUMMERATION_TIMEOUT);
            searchConnection.connect(masterBrickIP, DEFAULT_PORT);
            searchConnection.addEnumerateListener(new SensorSearcher(masterBrickIP));
            searchConnection.enumerate();
        } catch (NotConnectedException e) {
            logger.error("NotConnectedException by searchConnection{" + masterBrickIP + "}", e);
        } catch (UnknownHostException e) {
            logger.error("UnknownHostException", e);
        } catch (AlreadyConnectedException e) {
            logger.error("AlreadyConnectedException", e);
        } catch (IOException e) {
            logger.error("IOException", e);
        }
        return searchConnection;
    }

    protected void waitForTheSensorsToConnect(int amountOfExpectedSensors) {
        long maxTimeout = System.currentTimeMillis() + ENUMMERATION_TIMEOUT;
        while (amountOfExpectedSensors != sensorDefinitions.size() && System.currentTimeMillis() < maxTimeout) {
            // busy waiting (but only for a short amount of time)
        }
    }

    private ArrayList<TFSensor> createTFSensors() {
        ArrayList<TFSensor> rv = new ArrayList<TFSensor>();
        if (this.sensorDefinitions != null) {
            if (this.sensorDefinitions.size() < 3) {
                logger.warn("List of Sensordefinitions was < 3; Something went wrong...");
            }
            for (TFSensorConfigDefinition sensorDefinition : this.sensorDefinitions) {
                String uid = sensorDefinition.getUid();
                String host = sensorDefinition.getHost();
                int port = sensorDefinition.getPort();
                long callBackPeriod = sensorDefinition.getCallBackPeriod();
                IPConnection ipcon = new IPConnection();
                switch (sensorDefinition.getSensorType()) {
                case AMBIENT:
                    logger.info("Creating new Ambinetsensor: {uid=" + uid + " host=" + host + "}");
                    rv.add(new REMSAmbientLightSensor(uid, ipcon, host, port, persister, callBackPeriod));
                    break;
                case TEMPERATURE:
                    logger.info("Creating new Tempersturesensor: {uid=" + uid + " host=" + host + "}");
                    rv.add(new REMSTempSensor(uid, ipcon, host, port, persister, callBackPeriod));
                    break;
                case HUMIDITY:
                    logger.info("Creating new Humiditysensor: {uid=" + uid + " host=" + host + "}");
                    rv.add(new REMSHumiditySensor(uid, ipcon, host, port, persister, callBackPeriod));
                    break;
                case TEMPERATURE_IR:
                    logger.info("Creating new Temperature_IR: {uid=" + uid + " host=" + host + "}");
                    rv.add(new REMSTemperatureIRSensor(uid, ipcon, host, port, persister, callBackPeriod));
                    break;
                case MOISTURE:
                    logger.info("Creating new MoistureSensor: {uid=" + uid + " host=" + host + "}");
                    rv.add(new REMSMoistureSensor(uid, ipcon, host, port, persister, callBackPeriod));
                    break;
                case DISTANCE_IR:
                    logger.info("Creating new DISTANCE_IR: {uid=" + uid + " host=" + host + "}");
                    rv.add(new REMSDistanceIRSensor(uid, ipcon, host, port, persister, callBackPeriod));
                    break;
                case MOTION_DETECTOR:
                    logger.info("Creating new MOTION_DETECTOR: {uid=" + uid + " host=" + host + "}");
                    rv.add(new REMSMotionSensor(uid, ipcon, host, port, persister, callBackPeriod));
                    break;
                default:
                    logger.error("Sensortype {" + sensorDefinition.getSensorType()
                            + "} is not implementet in this version");
                    break;
                }
            }
            return rv;
        } else {
            logger.error("Sensordefinitions was Null -> createTFSensors() was called before updateSensorList()");
        }
        return rv;
    }

    
    @Override
    public String toString() {
        return "TFSensorController{Anzahl Sensoren(Conf)="+sensorDefinitions.size()+"}";
    }


    private class SensorSearcher implements EnumerateListener {

        private static final int FIRST_BRICKLET_IDENDIFIER = 21;
        private String ip;

        private SensorSearcher(String masterBrickIP) {
            this.ip = masterBrickIP;
        }

        @Override
        public void enumerate(String uid, String connectedUid, char position, short[] hardwareVersion,
                short[] firmwareVersion, int deviceIdentifier, short enumerationType) {

            if (deviceIdentifier >= FIRST_BRICKLET_IDENDIFIER) {
                // WE dont use to check Bricks (Masterbrick)

                TFSensorConfigDefinition foundSensor = new TFSensorConfigDefinition(
                        TFSensorType.getByDeviceIdentifier(deviceIdentifier), uid, ip, connectedUid, CALL_BACK_PERIOD);

                logger.debug("FoundSensor: " + foundSensor);
                if (!sensorDefinitions.contains(foundSensor)) {
                    logger.debug("Add to SensorList: " + foundSensor);
                    sensorDefinitions.add(foundSensor);
                }
            }
        }
    }
}
